package org.budo.java.agent.transformer.javassist;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.budo.java.agent.transformer.BudoClassFileTransformer;
import org.budo.support.lang.util.ReflectUtil;

import javassist.CtClass;

/**
 * @author lmw
 */
public abstract class AbstractJavassistClassFileTransformer extends BudoClassFileTransformer {
    private final Map<CtClass, String[]> methodNamesMap = new ConcurrentHashMap<CtClass, String[]>();

    private final Map<CtClass, CtClass[][]> methodParameterTypesMap = new ConcurrentHashMap<CtClass, CtClass[][]>();

    protected void putMethodName(CtClass type, String[] methodNames) {
        methodNamesMap.put(type, methodNames);
    }

    protected void putMethodParameterTypes(CtClass type, CtClass[][] methodParameterTypes) {
        methodParameterTypesMap.put(type, methodParameterTypes);
    }

    protected String toPrimitiveValue(CtClass type, String value) {
        String typeName = type.getName();

        if (typeName.equals("boolean")) {
            return " ((java.lang.Boolean)(" + value + ")).booleanValue() ";
        }

        if (typeName.equals("byte")) {
            return " ((java.lang.Byte)(" + value + ").byteValue() ";
        }

        if (typeName.equals("short")) {
            return " ((java.lang.Short)(" + value + ").shortValue() ";
        }

        if (typeName.equals("int")) {
            return "((java.lang.Integer)(" + value + ")).intValue() ";
        }

        if (typeName.equals("long")) {
            return " ((java.lang.Long)(" + value + ").longValue() ";
        }

        if (typeName.equals("char")) {
            return " ((java.lang.Character)(" + value + ").charValue() ";
        }
        if (typeName.equals("float")) {
            return " ((java.lang.Float)(" + value + ").floatValue() ";
        }

        if (typeName.equals("double")) {
            return " ((java.lang.Double)(" + value + ").doubleValue() ";
        }

        return " (" + typeName + ")(" + value + ") ";
    }

    protected String toObjectValue(CtClass type, String value) {
        String typeName = type.getName();
        if (typeName.equals("boolean")) {
            return " new java.lang.Boolean(" + value + ") ";
        }

        if (typeName.equals("byte")) {
            return " new java.lang.Byte(" + value + ") ";
        }

        if (typeName.equals("short")) {
            return " new java.lang.Short(" + value + ") ";
        }

        if (typeName.equals("int")) {
            return " new java.lang.Integer(" + value + ") ";
        }

        if (typeName.equals("long")) {
            return " new java.lang.Long(" + value + ") ";
        }

        if (typeName.equals("char")) {
            return " new java.lang.Character(" + value + ") ";
        }

        if (typeName.equals("float")) {
            return " new java.lang.Float(" + value + ") ";
        }

        if (typeName.equals("double")) {
            return " new java.lang.Double(" + value + ") ";
        }

        return " (" + typeName + ")(" + value + ") ";
    }

    @Override
    public Method getMethod(Class<?> targetType, Integer methodIndex) {
        return this.findMethod(targetType, methodIndex); // do cache
    }

    private Method findMethod(Class<?> targetType, Integer methodIndex) {
        String methodName = this.findMethodName(targetType, methodIndex);
        Class<?>[] parameterTypes = this.findParameterTypes(targetType, methodIndex);
        return ReflectUtil.getMethod(targetType, methodName, parameterTypes);
    }

    private Class<?>[] findParameterTypes(Class<?> targetType, Integer methodIndex) {
        Set<Entry<CtClass, CtClass[][]>> entrySet = methodParameterTypesMap.entrySet();
        for (Entry<CtClass, CtClass[][]> entry : entrySet) {
            CtClass key = entry.getKey();
            if (key.getName().equals(targetType.getName())) {
                CtClass[][] value = entry.getValue();
                CtClass[] ctTypes = value[methodIndex];
                return this.toJavaClass(ctTypes);
            }
        }

        throw new RuntimeException("targetType=" + targetType + ", methodIndex=" + methodIndex);
    }

    private String findMethodName(Class<?> targetType, Integer methodIndex) {
        for (Entry<CtClass, String[]> entry : methodNamesMap.entrySet()) {
            CtClass key = entry.getKey();
            if (key.getName().equals(targetType.getName())) {
                String[] value = entry.getValue();
                return value[methodIndex];
            }
        }

        throw new RuntimeException("targetType=" + targetType + ", methodIndex=" + methodIndex + ", methodNamesMap=" + methodNamesMap);
    }

    private Class[] toJavaClass(CtClass[] ctTypes) {
        if (null == ctTypes) {
            return null;
        }

        Class[] types = new Class[ctTypes.length];
        for (int i = 0; i < ctTypes.length; i++) {
            types[i] = this.toJavaClass(ctTypes[i]);
        }
        return types;
    }

    private Class toJavaClass(CtClass ctClass) {
        if (null == ctClass) {
            return null;
        }

        if ("int".equals(ctClass.getName())) {
            return int.class;
        }

        try {
            return ctClass.toClass();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}